<?php
/**
 * Webappkit API
 *
 * This file is part of Webappkit, a packages system for PHP web apps
 * @link http://webappkit.net
 * @package webappkit
 * @author J.Ducastel <nospam0@ducastel.name>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */

if (class_exists('webappkit'))
    return;

/**
 * @static
 */
class Webappkit {
/*--------------------------------------------------------------------
 STATIC METHODS
--------------------------------------------------------------------*/

    /**
    * factory : use this to get a kit instance.
    * @param string $id kit id : use / to access subkits
    * @static
    * @return webappkit
    */
    function & getKit($id) {
        // cleaning id
        $id=Webappkit::checkId($id);
        // storing instances in a static array
        static $kits=array();
        if (!isset($kits[$id])) {
            // checking kit
            if (!Webappkit::checkKit($id)) {
                // trigger_error("Webappkit : Cannot build kit $id",E_USER_WARNING);
                return false;
            }
            // loading kit libraries
            if (!webappkit::loadKit($id)) {
                // trigger_error("Webappkit : Cannot build kit $id",E_USER_WARNING);
                $kits[$id]=false;
            } else {
                // defining kit's class (default is webappkit)
                $cfg=webappkit::getCfgFromId($id);
                $class=isset($cfg['kit']['class'])?$cfg['kit']['class']:'WakKit';
                // building instance of correct class
                if ($class=='webappkit') {
                    $kit=&new webappkit($id);
                } else if (!class_exists($class)) {
                    trigger_error("Kit class $class does not exists for kit $id, defaulting to webappkit",E_USER_WARNING);
                    $kit=&new webappkit($id);
                } else {
                    $kit=&new $class($id);
                    if (!is_a($kit,'WakKit')) {
                        // the specified class does not extend webappkit !
                        trigger_error("Class $class does not extends WakKit, defaulting to WakKit",E_USER_WARNING);
                        $kit=&new webappkit($id);
                    }
                }
                // storing instance in cache
                $kits[$id]=&$kit;
            }
        }
        return $kits[$id];
    }

    /**
     * loads a kit libraries and dependancies
     * @access public
     * @static
     * @param string $id
     * @return string loaded version string
     */
    function loadKit($kit_id) {
        // cleaning id
        if (!$id=Webappkit::checkId($kit_id)) {
            trigger_error("Webappkit::loadKit(id): id $kit_id is not valid",E_USER_WARNING);
            return false;
        }
        static $loaded=array();
        if ($id and !isset($loaded[$id])) {
            $path=webappkit::getPathFromId($id);
            // reading meta
            if (!$cfg=webappkit::getCfgFromId($id)) {
                // unable to read config
                trigger_error("Webappkit : Cannot load kit $id, unable to read config file",E_USER_WARNING);
                // print_r($cfg);
                return false;
            };
            // checking for supported php version
            if (isset($cfg['kit']['phpversion'])
            and !webappkit::isVersionWithinRange(phpversion(),$cfg['kit']['phpversion'])) {
                trigger_error("Kit $id does not support PHP version ".phpversion(),E_USER_WARNING);
                return false;
            }
            // loading external libraries (kits dependancies)
            if (is_array($cfg['load_kits'])) {
                foreach ($cfg['load_kits'] as $required => $range) {
                    // parsing require statement : "id version-range"
                    $kit=$required;
                    if ($kit==$id) {
                        // preventing infinite loop
                        // trigger_error("Kit $id requires itself !",E_USER_WARNING);
                        continue;
                        // return false;
                    }
                    if (!webappkit::loadkit($kit)) {
                        trigger_error("could not load kit $kit which is required by $id");
                    }
                }
            }
            // loading internal libraries
            if (is_array($cfg['load_files'])) {
                foreach ($cfg['load_files'] as $file)
                    webappkit::loadlibfile($path.$file);
            }
            // loading internal classes (no autoload in php4)
            if (is_array($cfg['load_classes'])) {
                foreach ($cfg['load_classes'] as $file)
                    webappkit::loadlibfile($path.$file);
            }
            // loading subkits
            if (is_array($cfg['load_subkits'])) {
                foreach ($cfg['load_subkits'] as $subkit => $range) {
                    // echo "<p>loading $subkit subkit from $id</p>";
                    webappkit::loadkit($id.'.'.$subkit);
                }
            }
            $version=isset($cfg['kit']['version'])?$cfg['kit']['version']:'0.0.undefined';
            $loaded[$id]=$version;
            // return true;
        }
        return $loaded[$id];
    }

    /**
    * returns spent time in microseconds, with chosen precision
    * if not defined, defines WAK_START_TIME as reference microtime
    * @param int $precision (default 5)
    * @static
    * @access public
    * @return float spent time since the first call
    */
    function timeCounter($precision=5) {
        list($usec, $sec) = explode(" ", microtime());
        $microtime=((float)$usec + (float)$sec);
        if (!defined('WAK_START_TIME'))
            define('WAK_START_TIME',$microtime);
        //echo $microtime;
        $timeCounter=$microtime-WAK_START_TIME;
        return round($timeCounter,$precision);
    }

    /**
     * lists kits in a path. Uses root path as default
     * @static
     * @param string $path
     * @return array kits ids
     */
    function listKits($path=null) {
        if (is_null($path))
            $path=WAK_ROOT_PATH;
        if (!is_dir($path))
            return false;
        $path=realpath($path);
        $kits=array();
        $dir=dir($path);
        while ($file=$dir->read()) {
            if (is_dir($path.'/'.$file)
            and substr($file,-4,4)=='.wak') {
                // looks like a kit
                $id=substr($file,0,-4);
                $kits[]=$id;
            }
        }
        return $kits;
    }

    /**
     * checks for kit id
     * @static
     * @param string $id
     * @return bool true if kit exists and is valid, false elsewhere
     */
    function checkKit($id) {
        if (!$id=Webappkit::checkId($id)
        or !$path=Webappkit::getPathFromId($id)
        or !file_exists($path.'wak.ini'))
            return false;
        else
            return true;
    }

    /**
    * checks/cleans kit ID string format
    * accepts both / and . as separators for nesting levels
    * converts / to . in output
    * @static
    * @access public
    * @param string
    * @return string or false if id is invalid
    */
    function checkID($id) {
        $id=str_replace('/','.',strtolower($id));
        if (!ereg('^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$',$id))
            return false;
        return $id;
    }

    /**
    * get kit's physical path from kit id
    * @static
    * @access public
    * @param string $id use / or . to access subkits
    * @return string path with trailing slash
    */
    function getPathFromId($id) {
        if (!$id=Webappkit::checkID($id))
            return false;
        $levels=explode('.',$id);
        $path='';
        foreach ($levels as $l)
            $path.="$l.wak/";
        $path=WAK_ROOT_PATH.$path;
        if (!file_exists($path)) {
            // trigger_error("There is no path {$path} for kit {$id}");
            return false;
        }
        return $path;
    }

    /**
    * Recursive array merge, replacing values.
    * merges recursively array_1 with array_2, replacing array_1 keys by array_2 keys on collisions.
    * @static
    * @param array &$array_1
    * @param array &$array_2
    * @return &array merged result
    */
    function & mergeArrays(&$array_1,&$array_2) {
        if (!is_array($array_1) or !is_array($array_2)) {
            echo var_dump($array_1);
            echo var_dump($array_2);
            exit;
        }
        $merge=$array_1;
        $keys_1=array_keys($array_1);
        $keys_2=array_keys($array_2);
        foreach ($merge as $key => $value) {
            if (!isset($array_2[$key]))
                continue;
            if (is_array($value) and is_array($array_2[$key])) {
                // appel recursif
                $merge[$key]=Webappkit::mergeArrays($value,$array_2[$key]);
            } else {
                $merge[$key]=$array_2[$key];
            }
        }
        foreach ($array_2 as $key => $value)
            if (!isset($merge[$key])) $merge[$key]=$value;
        return $merge;
    }

    /**
    * loads package/kit metadata from ini file
    * @access public
    * @param string $id
    * @return array
    * @todo checking content format
    */
    function getCfgFromId($id) {
        if (!$id=Webappkit::checkID($id))
            return false;
        // searching in cache
        static $cfgs=array();
        if (!isset($cfgs[$id])) {
            // default config
            $default=array(
                'kit'=>array(
                    'version'=>'0.0'
                    , 'desc'=>'There should be a short description there')
                ,'load_kits'=>array()
                ,'load_files'=>array()
                ,'load_classes'=>array()
                ,'load_subkits'=>array()
                ,'tools'=>array());
            // getting kit's path
            if (!$path=Webappkit::getPathFromId($id))
                return false;
            // getting ini file
            $file=$path.'wak.ini';
            if (!file_exists($file) or !is_file($file)) {
                //trigger_error("kit metadata file $file not found",E_USER_WARNING);
                return false;
            }
                //throw new Exception("package info file $file not found");
            // parsing
            if (!$cfg=parse_ini_file($file,true)) {
                //throw new Exception("failed to parse $file");
                // trigger_error("failed to parse $file");
                return false;
            }
            $cfg=Webappkit::mergeArrays($default,$cfg);
            // echo '<pre>'; print_r($cfg);
            $cfgs[$id]=$cfg;
        }
        return $cfgs[$id];
    }

    /**
     * loads a library file.
     * can switch file upon current php version : if $file ends by php*, will load .php4, .php5 etc
     * @param string $file full physical path
     * @return bool
     */
    function loadLibFile($file) {
        static $loaded=array();
        if (in_array($file,$loaded)) // already loaded
            return true;
        // auto php version ?
        if (substr($file,-1,1)=='*') {
            $php=substr(phpversion(),0,1);
            $file=substr($file,0,-1).$php;
        }
        // checking existence
        if (!file_exists($file)) {
            trigger_error("can't include file $file, don't exist");
            return false;
        }
        global $WAK_LOADED_SIZE;
        $WAK_LOADED_SIZE+=filesize($file);
        include($file); //echo "<p>included $file</p>";
        $loaded[]=$file;
        return true;
    }

    /**
     * checks that a version string is within version range
     * @static
     * @access public
     * @param string $version version string to check x or x.y or x.y.* etc
     * @param string $range version range min-max or minAndMax or min+ or max-
     * @return bool true if in range, false elsewhere
     */
    function isVersionWithinRange($version,$range) {
        // parsing range
        switch (true) {
            case ereg('^[0-9\.\*]+$',$range): // minAndMax (with *=jokers)
                $min=$range; $max=$range;
                break;
            case substr($range,-1,1)=='+': // min+
                $min=substr($range,0,-1);
                $max=null;
                break;
            default: // min-max
                $range=explode('-',$range);
                $min=$range[0];
                $max=isset($range[1])?$range[1]:null;
        }
        // echo "<p>$min <= $version <= $max ?</p>";
        // is under minimum ?
        if (webappkit::cmpVersionStrings($version,$min)<0)
            return false; // under min
        // is over max ?
        if ($max and webappkit::cmpVersionStrings($max,$version)<0)
            return false;
        // is within range
        return true;
    }

    /**
     * compares version strings
     * @static
     * @access public
     * @param string $version1
     * @param string $version2
     * @return int 1 if $version1>$version2, 0 if $version1==$version2, -1 if $version1<$version2
     */
    function cmpVersionStrings($version1,$version2) {
        $v1=explode('.',$version1);
        $v2=explode('.',$version2);
        // looping through segments
        for ($n=0; $n<count($v1); $n++) {
            switch (true) {
                case ($v1[$n]=='*'): // joker, versions match
                    return 0;
                case !isset($v1[$n]): // v2 longer than v1, so over
                    return -1;
                case !isset($v2[$n]): // v1 longer than v2, so over
                    return 1;
                case $v1[$n]>$v2[$n]: // v1 over v2
                    return 1;
                case $v1[$n]<$v2[$n]: // v2 over v1
                    return -1;
                case $v1[$n]==$v2[$n]: // segment match, continue
                    continue;
            }
        }
        // versions are identical
        return 0;
    }
}
